require 'time'

# Basic OOP: Question 1

class Numeric
  def squared()
    return self**2
  end
end
# Test code: puts 9.squared()

# Basic OOP: Question 2
# Why must we say Time.parse("1:00 pm") but we can't say "1:00 pm".parse ?
# Because parse is a method of Time class, not a method of String class.

# What could you do if you wanted to able to use the latter syntax?
class String
  def parse
    return Time.parse(self)
  end
end
# Test code: puts "10:00pm".parse()
# But this syntax is a bit weird in that parsing String is very general thing to do,
# and we might want to parse it to something else like AST, XML, HTML, etc...

class Movie
  attr_reader :name
  attr_reader :showtime
  def initialize(name,showtime)
    @name = name
    case showtime
      when Time
        @showtime = showtime
      when String
        @showtime = Time.parse(showtime)
      else
        raise ArgumentError
      end
  end
  def name_and_showtime()
    if (@showtime > Time.now)
      return @name+": next show at "+@showtime.strftime("%I:%M%p")
    elsif (@showtime < Time.now)
      return @name+": already started at "+@showtime.strftime("%I:%M%p")
    else
      return @name+": starting right now"
    end
  end
  def <=>(movie)
    return @showtime.<=>(movie.showtime)
  end
  include Comparable
end
# Tested with
# Movie.new("Orphan", Time.parse("1:00 pm"))
# Movie.new("Orphan", "1:00 pm")
# Movie.new("Orphan", 123)

movies = []
movies << Movie.new("Orphan", Time.parse("1:00 pm"))
movies << Movie.new("Orphan", Time.parse("4:05 pm"))
movies << Movie.new("Orphan", Time.parse("7:00 pm"))
movies << Movie.new("GI Joe", Time.parse("1:05 pm"))
movies << Movie.new("GI Joe", Time.parse("4:30 pm"))
movies << Movie.new("GI Joe", Time.parse("6:50 pm"))
movies << Movie.new("Rocky Horror", Time.parse("12:00 am"))
movies << Movie.new("District 9", Time.parse("11:00 am"))
movies << Movie.new("District 9", Time.parse("3:00 pm"))

# Tests with
# puts movies[0]<=>movies[0]
# puts movies[0]<=>movies[1]
# puts movies[1]<=>movies[0]
# puts movies[0] < movies[1]
# puts movies[0] >= movies[1]
# puts movies[0] >= movies[0]
# puts movies[1].between?(movies[0],movies[1])
# movies2 = movies.sort()
# movies2.each {|x| puts x.showtime}

# Modules 3
# sort() is defined in Enumerable, which is included in Array class.
# Because 'movies' is an array of Movie class, Movie objects' being comparable
# is a sufficicent condition for 'movies' to be sortable


# Higher-order functions & functional programming
# 1.
module Enumerable
  def group_by
    result = {}
    values = self.map { |x|
        key = yield x
        result[key] = (result[key] ? result[key] : []) + [x]
      }
    return result
  end
end

# Tested with
# a = %w(apple banana cherry alice bob carol alpha bravo charlie)
# a.group_by {|x| x[0,1]}

# 2.
puts "Moive shows grouped by names"
grouped_movies = movies.group_by {|movie| movie.name}
grouped_movies["Orphan"].each {|x| puts x.name_and_showtime()}
grouped_movies["GI Joe"].each {|x| puts x.name_and_showtime()}
grouped_movies["Rocky Horror"].each {|x| puts x.name_and_showtime()}
grouped_movies["District 9"].each {|x| puts x.name_and_showtime()}

# 3.
puts
puts "Movies starting after 5pm:"
b = movies.find_all {|movie| movie.showtime > Time.parse("5:00pm")} # answer to 3-1
b.each {|x| puts x.name_and_showtime()}

puts # solution to 3-2
print "There are ", (movies.find_all {|movie| movie.showtime > Time.parse("5:00pm")}).length()
puts "movie(s) starting after 5pm"

puts # solution to 3-3
puts ((movies.find_all {|movie| movie.showtime > Time.parse("5:00pm")}).collect {|movie| movie.name}).uniq


# Iterators
# 1.
module Enumerable
  def each_even
    self.each do |x| yield(x) if x%2 == 0 end
  end
end

# Tested with (1..9).each_even { |n| puts n }

# 2.
module Enumerable
  def each_grouped_by_first_letter
    result = {}
    values = self.map { |x|
        key = x[0,1]
        result[key] = (result[key] ? result[key] : []) + [x]
      }
    result.each {|key, list| yield(key, list)}
  end
end

# Tested with
# a = %w(apple banana cherry alice bob carol alpha bravo charlie)
# a.each_grouped_by_first_letter do |letter, words|
#   puts "#{letter} : #{words.join(' ')}"
# end


class IndifferentHash < Hash

  def [](key); return super(getkey_s(key)); end
  def fetch(*args)
    if (args.length() == 2)
      return super(getkey_s(args[0]), args[1])
    else
      return super(getkey_s(args[0]))
    end
  end
  def []=(key, value); return super(getkey_s(key), value); end
  def store(key, value); return super(getkey_s(key), value); end
  def delete(key); return super(getkey_s(key)); end
  def has_key?(key); return super(getkey_s(key)); end
  def include?(key); return super(getkey_s(key)); end
  def key?(key); return super(getkey_s(key)); end
  def member?(key); return super(getkey_s(key)); end
  def values_at(*keys); return super(*(keys.map {|key| getkey_s(key)})); end

  private

  def getkey_s(key)
    if (key.is_a? Symbol)
      return key.to_s
    end
    return key
  end

end

# Tested with
#
# a = IndifferentHash.new
# a["foo"] = 3
# puts a["foo"]
# a[:bar] = 5
# puts a[:bar]
# a.store :baz, 7
# puts a[:foo]
# puts a["bar"]
# a.delete(:foo)
# puts a["foo"]
# puts a.fetch("foo", 11)
# puts a.fetch("foo") {13}
# puts a.has_key?(:bar) and a.include?(:bar) and a.key?(:bar) and a.key?(:bar)
# puts "[", a.values_at("bar", :baz), "]"


# Language extensions for convenience

# 1
x=""
puts x.empty?
x="   "
puts x.empty?
begin; puts nil.empty?; rescue; puts "exception occured"; end

# 2-4
class Object
  def blank?
    if self.respond_to?(:empty?)
      return self.empty?
    else
      return self.nil?
    end
  end
end

class String
  def blank?
    return super
  end
end

# Tested with
# puts "testing blank?"
# puts nil.blank? and "".blank? and [].blank? and {}.blank? and not [1].blank? and not {'a'=>nil}.blank?
